/**
 * @file ESP8266WSYBoot.cpp
 * @author BH3PXN
 * @brief 本程序修改自BG1REN的源码
 *        https://github.com/qiwenmin/esp8266-ir-gateway-bemfa
 * @version 0.1
 * @date 2022-03-23
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include "ESP8266WSYBoot.h"

#define LED_PATTERN_OFF 0x00000000
#define LED_PATTERN_ON 0xFFFFFFFF
#define LED_PATTERN_FAST 0x55555555
#define LED_PATTERN_SLOW 0x00FF00FF
#define LED_PATTERN_HEART 0x00000005
#define LED_PATTERN_WAIT 0x00000F0F

ESP8266WSYBoot::ESP8266WSYBoot() : _led_pin(PIN_NONE),
                                   _led_on_val(LOW),
                                   _btn_pin(PIN_NONE),
                                   _btn_down_val(LOW),
                                   _default_ssid(""),
                                   _default_pwd(""),
                                   _hostname("") {}

void ESP8266WSYBoot::setLed(uint8_t pin, uint8_t on_val) {
    _led_pin = pin;
    _led_on_val = on_val;
}

void ESP8266WSYBoot::begin() {
    if (!_hostname.length()) {
        _hostname = "SY" + String(ESP.getChipId(), DEC);
    }
    _setup_pins();
    _setup_littlefs();
    _setup_OTA();
    WiFi.hostname(_hostname);
    String wifiInfo = _read_wifi_info();
    _ssid = wifiInfo.substring(0, wifiInfo.indexOf("#"));
    _pwd = wifiInfo.substring(wifiInfo.indexOf("#") + 1);
    DEBUG_LOG("[WBoot] ssid:");
    DEBUG_LOG_LN(_ssid);
    DEBUG_LOG("[WBoot] pwd:");
    DEBUG_LOG_LN(_pwd);    
    DEBUG_LOG_LN("[WBoot] begin");
}

void ESP8266WSYBoot::loop() {
    _wifi_reconnect();
    ArduinoOTA.handle();
}

void ESP8266WSYBoot::_setup_pins() {
    if (_led_pin != PIN_NONE) {
        pinMode(_led_pin, OUTPUT);
        digitalWrite(_led_pin, !_led_on_val);
        _led_start();
    }

    if (_btn_pin != PIN_NONE) {
        pinMode(_btn_pin, INPUT);
        btnClearFlag();
        _btn_start();
    }
}

void ESP8266WSYBoot::ledOff() { _update_led_pattern(LED_PATTERN_OFF); };
void ESP8266WSYBoot::ledOn() { _update_led_pattern(LED_PATTERN_ON); };
void ESP8266WSYBoot::ledFast() { _update_led_pattern(LED_PATTERN_FAST); };
void ESP8266WSYBoot::ledSlow() { _update_led_pattern(LED_PATTERN_SLOW); };
void ESP8266WSYBoot::ledHeart() { _update_led_pattern(LED_PATTERN_HEART); };
void ESP8266WSYBoot::ledWait() { _update_led_pattern(LED_PATTERN_WAIT); };

void ESP8266WSYBoot::_led_start() {
    if (!_led_ticker.active()) {
        ledOff();
        _led_ticker.attach_ms(100, _led_ticker_callback, this);
    }
};

void ESP8266WSYBoot::_led_stop() {
    _led_ticker.detach();
    _update_led_pattern(LED_PATTERN_OFF); // Off
    _led_ticker_callback(this);
};

void ESP8266WSYBoot::_update_led_pattern(uint32_t pattern) {
    _led_pattern = pattern;
};

void ESP8266WSYBoot::_led_ticker_callback(ESP8266WSYBoot *self) {
    static uint8_t _led_pattern_bits = 0;
    if (self->_led_pin != PIN_NONE) {
        _led_pattern_bits &= 0x1F;
        digitalWrite(self->_led_pin, (self->_led_pattern >> _led_pattern_bits++) & 0x01 ? self->_led_on_val : !self->_led_on_val);
    }
};

void ESP8266WSYBoot::setBtn(uint8_t pin, uint8_t down_val) {
    _btn_pin = pin;
    _btn_down_val = down_val;
};

void ESP8266WSYBoot::_btn_start() {
    if (!_btn_ticker.active()) {
        _btn_ticker.attach_ms(10, _btn_ticker_callback, this);
    }
};

void ESP8266WSYBoot::_btn_ticker_callback(ESP8266WSYBoot *self) {
    static unsigned char btnNow;
    static unsigned char btnLast = digitalRead(self->_btn_pin);
    static unsigned int keepTime = 0;

    btnNow = digitalRead(self->_btn_pin);

    //下降沿 清零记数器
    if (btnNow == self->_btn_down_val && btnLast == !self->_btn_down_val) {
        btnLast = btnNow;
        keepTime = 0;
        self->_btn_flag = BTN_NONE;
    }

    //低电平
    if (btnNow == self->_btn_down_val && btnLast == self->_btn_down_val) {
        if (++keepTime >= 500) {
            keepTime = 500;
            if (self->_btn_flag != BTN_LONG) {
                self->_btn_flag = BTN_LONG;
            }
        }
    }

    //上升沿
    if (btnNow == !self->_btn_down_val && btnLast == self->_btn_down_val) {
        btnLast = btnNow;
        //消抖 30ms
        if (keepTime < 3) return;

        //忽略长按
        if (keepTime >= 500) {
            self->_btn_flag = BTN_NONE;
            return;
        }

        //单击
        self->_btn_flag = BTN_CLICK;
    }
};

uint8_t ESP8266WSYBoot::btnFlag() {
    return _btn_flag;
};

void ESP8266WSYBoot::btnClearFlag() {
    _btn_flag = BTN_NONE;
};

//wifi==============================================
bool ESP8266WSYBoot::isConnected() {

    return _wifi_state;
}
void ESP8266WSYBoot::setDefaultWifi(String ssid, String pwd) {
    _default_ssid = ssid;
    _default_pwd = pwd;
}
void ESP8266WSYBoot::setWifi(String ssid, String pwd){
    _write_wifi_info(ssid,pwd);
}
void ESP8266WSYBoot::disconnect() {
    WiFi.disconnect();
    delay(500);
}

void ESP8266WSYBoot::_wifi_reconnect() {
    static uint8_t step = 0;
    static uint32_t t = millis();
    static uint8_t retry = 0;
    static bool isUserWifi = true;
    //AP MODE
    if (step == 6) {
        ledOn();
        if (_btn_flag == BTN_CLICK) {
            step = 0;
            return;
        }
        return;
    }

    if (step == 4) {
        ledFast();
        if (_btn_flag == BTN_LONG) {
            if (millis() - t >= 5000UL) {
                WiFi.disconnect();
                WiFi.mode(WIFI_AP);
                WiFi.softAP(_hostname);
                _setup_mDNS();

                
                
                DEBUG_LOG_LN("[WBoot] start ap mode...");
                t = millis();
                step = 6;
                //ESP.restart();
            }
            return;
        }

        DEBUG_LOG_LN("[WBoot] start airkiss...");

        WiFi.disconnect();
        WiFi.mode(WIFI_STA);
        WiFi.beginSmartConfig();
        
        password = "123456";
        savePassword("123456");

        t = millis();
        step = 5;
        return;
    }

    if (step == 5) {
        if (_btn_flag == BTN_CLICK) {
            DEBUG_LOG_LN("[WBoot] break airkiss");
            btnClearFlag();
            step = 0;
            return;
        }
        if (millis() - t < 5000) {
            return;
        }
        t = millis();
        DEBUG_LOG_LN("[WBoot] check smart config");
        if (WiFi.smartConfigDone()) {
            WiFi.setAutoConnect(false); //禁用自动连接
            WiFi.persistent(false);
            DEBUG_LOG_LN("[WBoot] wifi config is saved,restart now");
            _write_wifi_info(WiFi.SSID(), WiFi.psk());
            ledOff();
            step = 0;
            ESP.reset();
        }
        return;
    }

    if (_btn_flag == BTN_LONG) {
        t = millis();
        step = 4;
        return;
    }

    if (millis() - t < 1000) {
        return;
    }
    t = millis();

    if (step == 0) {
        if (WiFi.status() != WL_CONNECTED) {
            ledSlow();
            _wifi_state = false;
            step = 1;
        } else {
            _wifi_state = true;
        }
        return;
    }

    if (step == 1) {
        if (_ssid == "") {
            retry = 0;
            step = 2;
            return;
        }
        DEBUG_LOG_F("[WBoot] wifi connect user ssid:%s,pwd:%s\r\n", _ssid.c_str(), _pwd.c_str());
        WiFi.disconnect();
        WiFi.setAutoConnect(false);
        WiFi.mode(WIFI_STA);

        WiFi.begin(_ssid.c_str(), _pwd.c_str());
        retry = 0;
        step = 3;
        isUserWifi = true;
        _wifi_state = false;
        return;
    }

    if (step == 2) {
        DEBUG_LOG_F("[WBoot] wifi connect default ssid:%s,pwd:%s\r\n", _default_ssid.c_str(), _default_pwd.c_str());
        WiFi.disconnect();
        WiFi.setAutoConnect(false);
        WiFi.mode(WIFI_STA);
        WiFi.begin(_default_ssid.c_str(), _default_pwd.c_str());
        retry = 0;
        step = 3;
        isUserWifi = false;
        _wifi_state = false;
        return;
    }

    if (step == 3) {
        if (isUserWifi) {
            DEBUG_LOG_LN("[WBoot] connecting user wifi");
        } else {
            DEBUG_LOG_LN("[WBoot] connecting default wifi");
        }

        if (WiFi.status() == WL_CONNECTED) {
            _wifi_state = true;
            _setup_mDNS();
            ledOff();
            step = 0;
            DEBUG_LOG("[WBoot] wifi connected,infomation:");
            DEBUG_LOG_LN();
            DEBUG_LOG("        HOSTNAME:");
            DEBUG_LOG_LN(_hostname);
            DEBUG_LOG("            SSID:");
            DEBUG_LOG_LN(WiFi.SSID().c_str());
            DEBUG_LOG("             PWD:");
            DEBUG_LOG_LN(WiFi.psk().c_str());
            DEBUG_LOG("              IP:");
            DEBUG_LOG_LN(WiFi.localIP());
        }

        if (++retry >= 30) {
            if (isUserWifi) {
                step = 2;
            } else {
                step = 1;
            }
            DEBUG_LOG_LN("[WBoot] connect wifi tomeout");
            return;
        }

        return;
    }
}

void ESP8266WSYBoot::_setup_littlefs() {
    if (LittleFS.begin()) {
        return;
    }
    DEBUG_LOG_LN("[WBoot] LittleFS format");
    LittleFS.format();
    _write_wifi_info("HAM", "12345678");
    savePassword("123456");
}

String ESP8266WSYBoot::_read_wifi_info() {
    String data = "";
    File file = LittleFS.open("/wifi.txt", "r");
    if (!file) {
        DEBUG_LOG_LN("[WBoot] LittleFS Failed to open file for reading");
        return "";
    }
    while (file.available()) {
        char c = (char)file.read();
        data = data + c;
    }
    file.close();
    DEBUG_LOG("[WBoot] wifi.txt:>");
    DEBUG_LOG(data);
    DEBUG_LOG_LN("<");
    return data;
}

void ESP8266WSYBoot::_write_wifi_info(String ssid, String pwd) {
    String data = "";
    data = ssid + "#" + pwd;
    DEBUG_LOG_LN("NEW SSID:");
    DEBUG_LOG_LN(data.c_str());
    File file = LittleFS.open("/wifi.txt", "w");
    if (!file) {
        DEBUG_LOG_LN("[WBoot] LittleFS Failed to open file for writing");
        return;
    }
    if (file.print(data.c_str())) {
        DEBUG_LOG_LN("[WBoot] LittleFS File written");
    } else {
        DEBUG_LOG_LN("[WBoot] LittleFS Write failed");
    }
    delay(2000); // Make sure the CREATE and LASTWRITE times are different
    file.close();
}

String ESP8266WSYBoot::getPassword() {
    String data = "";
    File file = LittleFS.open("/admin.txt", "r");
    if (!file) {
        DEBUG_LOG_LN("[WBoot] admin.txt LittleFS Failed to open file for reading");
        return "";
    }
    while (file.available()) {
        char c = (char)file.read();
        data = data + c;
    }
    file.close();
    DEBUG_LOG("[WBoot] admin.txt:>");
    DEBUG_LOG(data);
    DEBUG_LOG_LN("<");
    return data;
}

void ESP8266WSYBoot::savePassword(String password) {
    String data = password;
    DEBUG_LOG_LN("NEW SSID:");
    DEBUG_LOG_LN(data.c_str());
    File file = LittleFS.open("/admin.txt", "w");
    if (!file) {
        DEBUG_LOG_LN("[WBoot] LittleFS Failed to open file for writing");
        return;
    }
    if (file.print(data.c_str())) {
        DEBUG_LOG_LN("[WBoot] LittleFS File written");
    } else {
        DEBUG_LOG_LN("[WBoot] LittleFS Write failed");
    }
    //delay(2000); // Make sure the CREATE and LASTWRITE times are different
    file.close();
}

void ESP8266WSYBoot::_setup_OTA() {
    ArduinoOTA.setHostname(_hostname.c_str());
    ArduinoOTA.setPassword(_hostname.c_str());
    ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) {
        // DEBUG_LOG_F("[WBoot] OTA Progress: %u%%\r", (progress / (total / 100)));
    });
    ArduinoOTA.begin();
}

void ESP8266WSYBoot::_setup_mDNS() {
    DEBUG_LOG_F("[WBoot] setting up MDNS responder!hostname:%s.local\r\n", _hostname.c_str());
    MDNS.end();
    for (int i = 0; i < 10; i++) {
        if (!MDNS.begin(_hostname.c_str())) {
            DEBUG_LOG_LN("[WBoot] Error setting up MDNS responder!");
        } else {
            DEBUG_LOG_LN("[WBoot] set mDNS sessues");
            break;
        }
    }
}

void ESP8266WSYBoot::setHostname(String hostname) {
    _hostname = hostname;
}

//http update =============================================================
void ESP8266WSYBoot::_update_started(ESP8266WSYBoot *self) {
    DEBUG_LOG_LN("CALLBACK:  HTTP update process started");
}

void ESP8266WSYBoot::_update_finished(ESP8266WSYBoot *self) {
    DEBUG_LOG_LN("CALLBACK:  HTTP update process finished");
}

void ESP8266WSYBoot::_update_progress(ESP8266WSYBoot *self, int cur, int total) {
    DEBUG_LOG_F("CALLBACK:  HTTP update process at %d of %d bytes...\n", cur, total);
}

void ESP8266WSYBoot::_update_error(ESP8266WSYBoot *self, int err) {
    DEBUG_LOG_F("CALLBACK:  HTTP update fatal error code %d\n", err);
}

void ESP8266WSYBoot::update_begin(String binUrl) {
    DEBUG_LOG("begin update with http");
    DEBUG_LOG_LN(binUrl);
    if (WiFi.status() != WL_CONNECTED) {
        return;
    }
    WiFiClient client;

    ESPhttpUpdate.setLedPin(_led_pin, _led_on_val);

    // Add optional callback notifiers
    ESPhttpUpdate.onStart([this]() { this->_update_started(this); });
    ESPhttpUpdate.onEnd([this]() { this->_update_finished(this); });

    ESPhttpUpdate.onProgress([this](int cur, int total) {
        this->_update_progress(this, cur, total);
    });

    ESPhttpUpdate.onError([this](int err) {
        this->_update_error(this, err);
    });

    t_httpUpdate_return ret = ESPhttpUpdate.update(client, binUrl);

    switch (ret) {
    case HTTP_UPDATE_FAILED:
        DEBUG_LOG_F("HTTP_UPDATE_FAILD Error (%d): %s\n",
                    ESPhttpUpdate.getLastError(),
                    ESPhttpUpdate.getLastErrorString().c_str());
        break;

    case HTTP_UPDATE_NO_UPDATES:
        DEBUG_LOG_LN("HTTP_UPDATE_NO_UPDATES");
        break;

    case HTTP_UPDATE_OK:
        DEBUG_LOG_LN("HTTP_UPDATE_OK");
        break;
    }
}